SpringBoot实现登录功能 | 您所在的位置:网站首页 › springboot 登录 › SpringBoot实现登录功能 |
1、前言
本文主要采用springboot+redis实现登录功能。持久层使用的是MyBatis Plus操作数据库。适合学完springboot,背过redis八股文的知识的实战练习。 效果图: 1.pom.xml :引入相关资源文件 org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 org.springframework.boot spring-boot-starter-web mysql mysql-connector-java runtime 8.0.20 org.projectlombok lombok true org.springframework.boot spring-boot-devtools runtime true org.springframework.boot spring-boot-starter-test test com.baomidou mybatis-plus-boot-starter 3.4.3 cn.hutool hutool-all 5.7.172.application.yaml: 配置文件 # 服务端口 server: port: 8081 spring: # 服务名称 application: name: hmdp # mysql数据源配置 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/hmdp?useSSL=false&serverTimezone=UTC&charactEncoding=UTF-8 username: root password: root # redis数据源配置 redis: host: 192.168.171.132 # 自己的虚拟机ip地址 port: 6379 password: 123456 lettuce: pool: max-active: 10 max-idle: 10 min-idle: 1 time-between-eviction-runs: 10s jackson: default-property-inclusion: non_null # JSON处理时忽略非空字段 mybatis-plus: type-aliases-package: com.hmdp.entity # 别名扫描包 logging: level: com.hmdp: debug # 日志打印3.基于session的验证码登录 1)发送验证码业务流程 2)用户登录业务流程 注意:校验登录功能存在的问题:User对象存到session造成内存泄露 解决:隐藏User对象的部分敏感属性 controller/UserController /** * 登录功能 * @RequestBody:{phone: "19956570011", code: "112211"}前端数据是json格式 * 后端自动解析json格式的数据 * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码 */ @PostMapping("/login") public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){ // 实现登录功能 return userService.login(loginForm, session); } service/UserServiceImpl //用户登录 @Override public Result login(LoginFormDTO loginForm, HttpSession session) { //校验手机号 String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)){ return Result.fail("手机号格式有误, 请重新输入!"); } //校验验证码 Object codeCache= session.getAttribute("code"); String code = loginForm.getCode(); if (codeCache == null || !codeCache.toString().equals(code)){ //验证码不一致,报错 return Result.fail("验证码错误"); } //一致,根据手机号查询用户user select * from tb_user where phone = ? User user = query().eq("phone", phone).one(); //判断用户是否存在 if (user == null) { //不存在,创建新用户。根据手机号创建用户 user = createUserWithPhone(phone); } //存在保存到session中 session.setAttribute("user", user); return Result.ok(); } private User createUserWithPhone(String phone) { //创建用户 User user = new User(); user.setPhone(phone); user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX +RandomUtil.randomString(6)); //保存用户 save(user); return user; }3)登录校验业务流程 intercepter/LoginIntercepter 拦截器对象 public class LoginIntercepter implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //从cookie获取session HttpSession session = request.getSession(); //从session中获取User对象 Object user = session.getAttribute("user"); //判断:用户是否存在 if (user == null){ // 如果不存在,拦截 状态码:401 30x 40x 50x response.setStatus(401); return false; } // 存在用户保存到ThreadLocal中 UserHolder.saveUser((UserDTO) user); //放行 return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //登录完成后移除用户 UserHolder.removeUser(); } }config/MyMvcConfig 配置登录拦截器 @Configuration public class MyMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginIntercepter()) .excludePathPatterns( "/blog/hot", "/shop/**", "/shop-type/**", "/user/code", "/user/login" ); } }controller/UserController @GetMapping("/me") public Result me(){ //获取当前登录的用户并返回 UserDTO user = UserHolder.getUser(); return Result.ok(user); }service/UserServiceImpl //存在保存到session中 将user对象转为UserDto 避免传递敏感信息 session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));4)用户退出登录 /** * 登出功能 * @return 无 */ @PostMapping("/logout") public Result logout(HttpSession session){ // 实现登出功能 session.removeAttribute("user"); return Result.ok("退出登录"); } 敏感信息(手机号、密码、身份证号、特殊时间点)传递的问题若存在User对象,将User对象保存到session中 方法1:手动new UserDto对象封装id、nickName、icon属性 方法2:将user对象转为UserDto,BeanUtil.copyProperties(user, UserDTO.class避免传递敏感信息 基于session的登录存在的问题 session共享问题:用户请求的一个服务模块可能需要调用到服务器B,当用户发起请求的时候,此时的服务器B上并没有存储该用户的sessionID,所以就会再次让用户进行一个登陆操作。还有可能会导致用户本来就想完成一个下单操作,但是却还登陆了好几次的情况。 session不共享问题:请求切换到不同 服务器导致数据丢失。 4.基于redis共享session登录 将手机号生成的验证码保存到redis,以手机号作为key存储到redis,String类型的value是验证码。验证码在redis设置过期时间。为了防止内存溢出。用户对象保存到redis,以随机token作为key,value可以是String类型json字符串,可以hash类型。用token获取用户实现数据共享。redis的用户生成token设置过期时间controller/UserController /** * 发送手机验证码 */ @PostMapping("code") public Result sendCode(@RequestParam("phone") String phone, HttpSession session) { // 发送短信验证码并保存验证码 return userService.sendCode(phone, session); } /** * 登录功能 * @RequestBody:{phone: "19956570011", code: "112211"}前端数据是json格式 * 后端自动解析json格式的数据 * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码 */ @PostMapping("/login") public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){ // 实现登录功能 return userService.login(loginForm, session); } /** * 校验登录功能 * @return 无 */ @GetMapping("/me") public Result me(){ //获取当前登录的用户并返回 UserDTO user = UserHolder.getUser(); return Result.ok(user); }service/UserServiceImpl //发送手机验证码 @Override public Result sendCode(String phone, HttpSession session) { //校验手机号 -- 正则表达式 if (RegexUtils.isPhoneInvalid(phone)){ //如果手机号不符合,就返回错误信息 return Result.fail("手机号格式有误, 请重新输入!"); } //符合,生成验证码 hutool-all工具包 String code = RandomUtil.randomNumbers(6); //验证码保存到redis中 验证码code选择 String类型 //设置 有效期 set key value ex //session.setAttribute("code", code); stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES); //发送验证码 -- 调用第三方技术的短信服务(参考阿里云视频点播的案例) 模拟发送短信成功 控制台打印日志模拟由短信服务上发送的验证码 log.debug("短信验证码发送成功,【" + code + "】"); return Result.ok(); } //用户登录 @Override public Result login(LoginFormDTO loginForm, HttpSession session) { //校验手机号 String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)){ return Result.fail("手机号格式有误, 请重新输入!"); } // 从redis获取验证码 校验验证码 //Object codeCache= session.getAttribute("code"); String codeCache = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone); String code = loginForm.getCode(); if (codeCache == null || !codeCache.toString().equals(code)){ //验证码不一致,报错 return Result.fail("验证码错误"); } //一致,根据手机号查询用户user select * from tb_user where phone = ? User user = query().eq("phone", phone).one(); //判断用户是否存在 if (user == null) { //不存在,创建新用户。根据手机号创建用户 user = createUserWithPhone(phone); } //若存在User对象,将User对象保存到session中 // 方法1:手动new UserDto对象封装id、nickName、icon属性 // 方法2:将user对象转为UserDto,BeanUtil.copyProperties(user, UserDTO.class避免传递敏感信息 //存在用户保存到redis //生成token String token = UUID.randomUUID().toString(true); //将User对象转换为hash存储 UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); //UserDTO对象属性保证String类型 Map userMap = BeanUtil.beanToMap(userDTO, new HashMap(), CopyOptions.create() .setIgnoreNullValue(false) .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); //存储 stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token, userMap); //设置token的有效期 stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES); //返回token return Result.ok(token); } private User createUserWithPhone(String phone) { //创建用户 User user = new User(); user.setPhone(phone); user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX +RandomUtil.randomString(6)); //保存用户 save(user); return user; }intercepter/LoginIntercepter 登录拦截器 private StringRedisTemplate stringRedisTemplate; public LoginIntercepter(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从请求头中获取token //HttpSession session = request.getSession(); String token = request.getHeader("authorization"); // 基于token获取redis的User对象 //Object user = session.getAttribute("user"); String key = RedisConstants.LOGIN_USER_KEY + token; Map userMap = stringRedisTemplate.opsForHash() .entries(key); //判断:用户是否存在 if (userMap.isEmpty()){ // 如果不存在,拦截 状态码:401 30x 40x 50x response.setStatus(401); return false; } // 将查询hash数据转换成UserDto对象 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 存在用户保存到ThreadLocal中 UserHolder.saveUser(userDTO); // 刷新token有效期 stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); //放行 return true; }登录信息存储到redis效果图 至此完成系统登录功能,还有优化的地方欢迎大家在评论区留言!我们一起学习。 |
CopyRight 2018-2019 实验室设备网 版权所有 |